@file:Suppress("unused")

package com.squareup.anvil.compiler.internal

import com.squareup.anvil.annotations.ExperimentalAnvilApi
import com.squareup.anvil.compiler.api.AnvilCompilationException
import com.squareup.anvil.compiler.internal.reference.AnnotatedReference
import com.squareup.anvil.compiler.internal.reference.TypeReference
import com.squareup.anvil.compiler.internal.reference.canResolveFqName
import com.squareup.kotlinpoet.ANY
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.Dynamic
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.jvm.jvmSuppressWildcards
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.descriptorUtil.parents
import org.jetbrains.kotlin.resolve.descriptorUtil.parentsWithSelf

@ExperimentalAnvilApi
public fun ClassDescriptor.asClassName(): ClassName =
  ClassName(
    packageName = parents.filterIsInstance<PackageFragmentDescriptor>().first()
      .fqName.safePackageString(dotSuffix = false),
    simpleNames = parentsWithSelf.filterIsInstance<ClassDescriptor>()
      .map { it.name.asString() }
      .toList()
      .reversed(),
  )

@ExperimentalAnvilApi
public fun FqName.asClassName(module: ModuleDescriptor): ClassName {
  return asClassNameOrNull(module)
    ?: throw AnvilCompilationException("Couldn't parse ClassName for $this.")
}

private fun FqName.asClassNameOrNull(module: ModuleDescriptor): ClassName? {
  val segments = pathSegments().map { it.asString() }

  // If the first sentence case is not the last segment of the path it becomes ambiguous,
  // for example, com.Foo.Bar could be an inner class Bar or an unconventional package com.Foo.
  val canGuessClassName = segments.indexOfFirst { it[0].isUpperCase() } == segments.size - 1
  if (canGuessClassName) {
    return ClassName.bestGuess(asString())
  }

  for (index in 1..segments.size) {
    // Try to resolve each segment chunk starting from the base package segment
    // e.g. try resolving 'com', then 'com.squareup', then 'com.squareup.Foo'
    val validFqName = FqName.fromSegments(segments.subList(0, index)).canResolveFqName(module)
    if (validFqName) {
      // If we found a valid name, it means the package is one segment back from the current index
      return ClassName(
        packageName = segments.subList(0, index - 1).joinToString(separator = "."),
        simpleNames = segments.subList(index - 1, segments.size),
      )
    }
  }

  // No matching class found. It's either a package name only or doesn't exist.
  return null
}

@ExperimentalAnvilApi
public fun ClassId.asClassName(): ClassName {
  return ClassName(
    packageName = packageFqName.asString(),
    simpleNames = relativeClassName.pathSegments().map { it.asString() },
  )
}

@ExperimentalAnvilApi
public fun TypeName.withJvmSuppressWildcardsIfNeeded(
  annotatedReference: AnnotatedReference,
  typeReference: TypeReference,
): TypeName {
  // If the parameter is annotated with @JvmSuppressWildcards, then add the annotation
  // to our type so that this information is forwarded when our Factory is compiled.
  val hasJvmSuppressWildcards = annotatedReference.isAnnotatedWith(jvmSuppressWildcardsFqName)

  // Add the @JvmSuppressWildcards annotation even for simple generic return types like
  // Set<String>. This avoids some edge cases where Dagger chokes.
  val isGenericType = typeReference.isGenericType()

  // Same for functions.
  val isFunctionType = typeReference.isFunctionType()

  return when {
    hasJvmSuppressWildcards || isGenericType -> this.jvmSuppressWildcards()
    isFunctionType -> this.jvmSuppressWildcards()
    else -> this
  }
}

private fun FileSpec.Builder.suppressWarnings() {
  addAnnotation(
    AnnotationSpec
      .builder(Suppress::class)
      // Suppress deprecation warnings.
      .addMember("\"DEPRECATION\"")
      // Suppress errors for experimental features in generated code.
      .apply {
        addMember("\"OPT_IN_USAGE\"")
        addMember("\"OPT_IN_USAGE_ERROR\"")
      }
      .build(),
  )
}

@ExperimentalAnvilApi
public fun FileSpec.Companion.buildFile(
  packageName: String,
  fileName: String,
  generatorComment: String = "Generated by Anvil.\nhttps://github.com/square/anvil",
  block: FileSpec.Builder.() -> Unit,
): String = createAnvilSpec(packageName, fileName, generatorComment, block)
  .toString()

@ExperimentalAnvilApi
public fun FileSpec.Companion.createAnvilSpec(
  packageName: String,
  fileName: String,
  generatorComment: String = "Generated by Anvil.\nhttps://github.com/square/anvil",
  block: FileSpec.Builder.() -> Unit,
): FileSpec =
  builder(packageName, fileName)
    .apply {
      // Suppress any deprecation warnings.
      suppressWarnings()

      block()
    }
    .addFileComment(generatorComment)
    .build()

/**
 * For `Map<String, Int>` this will return [`String`, `Int`]. For star projections like
 * `List<*>` the result will be mapped to [Any].
 */
public val TypeName.unwrappedTypes: List<TypeName>
  get() = when (this) {
    is ParameterizedTypeName -> typeArguments
    else -> emptyList()
  }

/**
 * Returns the result of [findRawType] or throws.
 */
public fun TypeName.requireRawType(): ClassName {
  return findRawType() ?: error("Cannot get raw type from $this")
}

/**
 * Returns the raw type for this [TypeName] or null if one can't be resolved.
 */
public fun TypeName.findRawType(): ClassName? {
  return when (this) {
    is ClassName -> this
    is ParameterizedTypeName -> rawType
    is TypeVariableName -> ANY
    is WildcardTypeName -> outTypes.first().findRawType()
    is LambdaTypeName -> {
      var count = parameters.size
      if (receiver != null) {
        count++
      }
      val functionSimpleName = if (count >= 23) {
        "FunctionN"
      } else {
        "Function$count"
      }
      ClassName("kotlin.jvm.functions", functionSimpleName)
    }
    Dynamic -> null
  }
}
